package main;

import java.util.ArrayList;
import java.util.HashMap;

import dataAccess.databaseManagement.entity.AssetEntity;
import dataAccess.databaseManagement.entity.PriceEntity;

public class MatchingAlgorithm {
	private int interval = 5;
	private int noSample = 5;
	
	private HashMap<AssetEntity, ArrayList<PriceEntity>> references = new HashMap<AssetEntity, ArrayList<PriceEntity>>();
	private ArrayList<PriceEntity> priceEntities; 
	
	public MatchingAlgorithm() {
	}
	
	public MatchingAlgorithm(int interval, int noSample, HashMap<AssetEntity, ArrayList<PriceEntity>> references, ArrayList<PriceEntity> priceEntities) {
		this.interval = interval;
		this.noSample = noSample;
		this.references = references;
		this.priceEntities = priceEntities;
	}
	
	public double[][] predict(int startPredictedDate, int noPredictedDates, int noExpandedDates) {
		
		double[][] results = new double[(int) Math.pow(2, noExpandedDates)][noPredictedDates];
	
		for (int i = startPredictedDate; i < startPredictedDate + noPredictedDates; ++i) {
			
			if (i - startPredictedDate < noExpandedDates) {
				for (int l = 0; l < (int) Math.pow(2, i - startPredictedDate + 1); ++l) {
					int n = (int) Math.pow(2, noExpandedDates - i + startPredictedDate - 1);
					double predictedValue = predictUpOrDown(i, startPredictedDate, results, l, n);
					int m = l*n;
					for (int j = 0; j < n; ++j) {
						results[m+j][i - startPredictedDate] = predictedValue;
					}
				}
			} else {
				predictAveragely(i, startPredictedDate, results);
			}
			
		}
		
		return results;
	}
	
	private double predictUpOrDown(int i, int startPredictedDate, double[][] results, int l, int n) {
		ArrayList<Double> matchingObject = new ArrayList<Double>();
		for (int j = i - interval; j < startPredictedDate; ++j) {
			matchingObject.add(priceEntities.get(j).getClose());
		}

		for (int j = i - interval; matchingObject.size() < interval; ++j) {
			if (j < startPredictedDate)
				continue;
			if (results[l*n][j - startPredictedDate] == 0) {
				return 0;
			} else {
				matchingObject.add(results[l*n][j - startPredictedDate]);
			}
		}

		ArrayList<Double> closePrices = normalize(matchingObject);

		ArrayList<ArrayList<Double>> theBestMatchings = new ArrayList<ArrayList<Double>>();
		ArrayList<Double> theBestPredicts = new ArrayList<Double>();
		ArrayList<Double> mins = new ArrayList<Double>();

		for (int j = 0; j < noSample; ++j) {
			mins.add(Double.MAX_VALUE);
			theBestMatchings.add(new ArrayList<Double>());
			theBestPredicts.add(new Double(0));
		}

		for (int j = 0; j < startPredictedDate - interval - 1; ++j) {
			ArrayList<Double> matchingResult = new ArrayList<Double>();
			for (int k = j; k < j + interval; ++k) {
				matchingResult.add(priceEntities.get(k).getClose());
			}

			ArrayList<Double> closePricesMatching = normalize(matchingResult);

			double distance = distance(closePrices,
					closePricesMatching);

			int insert;
			for (insert = 0; insert < noSample
					&& distance > mins.get(insert); ++insert)
				;

			if (insert < noSample) {
				for (int k = noSample - 1; k > insert; --k) {
					mins.set(k, mins.get(k - 1));
					theBestMatchings
							.set(k, theBestMatchings.get(k - 1));
					theBestPredicts.set(k, theBestPredicts.get(k - 1));
				}

				mins.set(insert, distance);
				theBestMatchings.set(insert, matchingResult);
				theBestPredicts.set(insert,
						priceEntities.get(j + interval).getClose());
			}
		}

		// references asset
		for (AssetEntity asset : references.keySet()) {
			ArrayList<PriceEntity> entityList = references.get(asset);

			int j = interval - 1;
			while (entityList.get(j).getDate().getTime() < priceEntities
					.get(startPredictedDate).getDate().getTime()
					&& j < entityList.size() - 1) {
				ArrayList<Double> matchingResult = new ArrayList<Double>();
				for (int k = j - interval + 1; k <= j; ++k) {
					matchingResult.add(entityList.get(k).getClose());
				}

				ArrayList<Double> closePricesMatching = normalize(matchingResult);

				double distance = distance(closePrices,
						closePricesMatching);

				int insert;
				for (insert = 0; insert < noSample
						&& distance > mins.get(insert); ++insert)
					;

				if (insert < noSample) {
					for (int k = noSample - 1; k > insert; --k) {
						mins.set(k, mins.get(k - 1));
						theBestMatchings.set(k,
								theBestMatchings.get(k - 1));
						theBestPredicts.set(k,
								theBestPredicts.get(k - 1));
					}

					mins.set(insert, distance);
					theBestMatchings.set(insert, matchingResult);
					theBestPredicts.set(insert, entityList.get(j + 1)
							.getClose());
				}

				++j;
			}
		}

		double returnROC = 0;
		
		//method one
//		{
//			int count = 0;
//	
//			if (l%2 == 0) {
//				for (int j = 0; j < noSample; ++j) {
//					double roc = theBestPredicts.get(j)
//							/ theBestMatchings.get(j).get(
//									theBestMatchings.get(j).size() - 1) - 1;
//					if (roc > 0) {
//						returnROC += roc;
//						++count;
//					}
//				}			
//			} else {
//				for (int j = 0; j < noSample; ++j) {
//					double roc = theBestPredicts.get(j)
//							/ theBestMatchings.get(j).get(
//									theBestMatchings.get(j).size() - 1) - 1;
//					if (roc < 0) {
//						returnROC += roc;
//						++count;
//					}
//				}			
//			}
//	
//			if (count == 0) {
//				return 0;
//			}
//			
//			returnROC /= count;
//		}
		
		//method two
		{
			double max = theBestPredicts.get(0)
					/ theBestMatchings.get(0).get(
							theBestMatchings.get(0).size() - 1) - 1, min = max;
			
			for (int j = 1; j < noSample; ++j) {
				double roc = theBestPredicts.get(j)
						/ theBestMatchings.get(j).get(
								theBestMatchings.get(j).size() - 1) - 1;
				if (roc > max) {
					max = roc;
				}
				
				if (roc < min) {
					min = roc;
				}
			}
			
			if (l%2 == 0) {
				returnROC = max;
			} else {
				returnROC = min;
			}
		}

		double result = 0;
		
		if (i - startPredictedDate == 0) {
			result = (priceEntities.get(i - 1)
					.getClose() + priceEntities.get(i - 1).getClose()
					* returnROC);				
		} else {
			result = (results[l*n][i - startPredictedDate-1] + results[l*n][i - startPredictedDate-1] * returnROC);
		}			

		return result;
	}

	private void predictAveragely(int i, int startPredictedDate, double[][] results) {
		L1: for (int l = 0; l < results.length; ++l) {
			ArrayList<Double> matchingObject = new ArrayList<Double>();
			
			for (int j = i - interval; j < startPredictedDate; ++j) {
				matchingObject.add(priceEntities.get(j).getClose());
			}

			for (int j = i - interval; matchingObject.size() < interval; ++j) {
				if (j < startPredictedDate)
					continue;
				if (results[l][j - startPredictedDate] == 0) {
					results[l][i - startPredictedDate] = 0;
					continue L1;
				}
				matchingObject.add(results[l][j - startPredictedDate]);
			}

			ArrayList<Double> closePrices = normalize(matchingObject);

			ArrayList<ArrayList<Double>> theBestMatchings = new ArrayList<ArrayList<Double>>();
			ArrayList<Double> theBestPredicts = new ArrayList<Double>();
			ArrayList<Double> mins = new ArrayList<Double>();

			for (int j = 0; j < noSample; ++j) {
				mins.add(Double.MAX_VALUE);
				theBestMatchings.add(new ArrayList<Double>());
				theBestPredicts.add(new Double(0));
			}

			for (int j = 0; j < startPredictedDate - interval; ++j) {
				ArrayList<Double> matchingResult = new ArrayList<Double>();
				for (int k = j; k < j + interval; ++k) {
					matchingResult.add(priceEntities.get(k).getClose());
				}

				ArrayList<Double> closePricesMatching = normalize(matchingResult);

				double distance = distance(closePrices, closePricesMatching);

				int insert;
				for (insert = 0; insert < noSample
						&& distance > mins.get(insert); ++insert)
					;

				if (insert < noSample) {
					for (int k = noSample - 1; k > insert; --k) {
						mins.set(k, mins.get(k - 1));
						theBestMatchings
								.set(k, theBestMatchings.get(k - 1));
						theBestPredicts.set(k, theBestPredicts.get(k - 1));
					}

					mins.set(insert, distance);
					theBestMatchings.set(insert, matchingResult);
					theBestPredicts.set(insert, priceEntities.get(j + interval).getClose());
				}
			}

			// references asset
			for (AssetEntity asset : references.keySet()) {
				ArrayList<PriceEntity> entityList = references.get(asset);

				int j = interval - 1;
				while (j < entityList.size() - 1 && entityList.get(j).getDate().getTime() <= priceEntities
						.get(startPredictedDate-1).getDate().getTime()) {
					ArrayList<Double> matchingResult = new ArrayList<Double>();
					for (int k = j - interval + 1; k <= j; ++k) {
						matchingResult.add(entityList.get(k).getClose());
					}

					ArrayList<Double> closePricesMatching = normalize(matchingResult);

					double distance = distance(closePrices,
							closePricesMatching);

					int insert;
					for (insert = 0; insert < noSample
							&& distance > mins.get(insert); ++insert)
						;

					if (insert < noSample) {
						for (int k = noSample - 1; k > insert; --k) {
							mins.set(k, mins.get(k - 1));
							theBestMatchings.set(k,
									theBestMatchings.get(k - 1));
							theBestPredicts.set(k,
									theBestPredicts.get(k - 1));
						}

						mins.set(insert, distance);
						theBestMatchings.set(insert, matchingResult);
						theBestPredicts.set(insert, entityList.get(j + 1)
								.getClose());
					}

					++j;
				}
			}

			double rateOfChange = 0;
			double total = 0;
			
			for (int j = 0; j < noSample; ++j) {
				if (theBestMatchings.get(j).size() == 0) {
					break;
				}
				double rate = theBestPredicts.get(j) / theBestMatchings.get(j).get(theBestMatchings.get(j).size() - 1) - 1;
				rateOfChange += rate/mins.get(j);
				total += 1.0/mins.get(j);
			}

			rateOfChange /= total;

			if (i - startPredictedDate == 0) {
				results[l][i - startPredictedDate] = (priceEntities.get(i - 1)
						.getClose() + priceEntities.get(i - 1).getClose()
						* rateOfChange);				
			} else {
				results[l][i - startPredictedDate] = (results[l][i - startPredictedDate-1] + results[l][i - startPredictedDate-1] * rateOfChange);
				
			}			
		}		
	}
	
	public double[][] findTheBestFit(int end) {
		double[][] result = new double[noSample+3][];
		double[] mins = new double[noSample+1];
		double[] mins2 = new double[noSample+1];
		
		ArrayList<Double> matchingObject = new ArrayList<Double>();
		for (int j = end-interval; j < end; ++j) {
			matchingObject.add(priceEntities.get(j).getClose());
		}

		ArrayList<Double> closePrices = normalize(matchingObject);
		ArrayList<Double> closePrices2 = normalize2(matchingObject);

		for (int j = 0; j <= noSample; ++j) {
			mins[j] = Double.MAX_VALUE;
			result[j] = new double[2];
		}
		
		mins[0] = 0;
		mins2[0] = 0;
		result[0][0] = end-interval;
		result[0][1] = end-1;

		for (int j = 0; j < end-interval; ++j) {
			ArrayList<Double> matchingResult = new ArrayList<Double>();
			for (int k = j; k < j + interval; ++k) {
				matchingResult.add(priceEntities.get(k).getClose());
			}
			ArrayList<Double> closePricesMatching = normalize(matchingResult);
			ArrayList<Double> closePricesMatching2 = normalize2(matchingResult);
			
			double distance = distance(closePrices, closePricesMatching);
			double distance2 = distance(closePrices2, closePricesMatching2);
			int insert;
			for (insert = 0; insert <= noSample && distance > mins[insert]; ++insert);

			if (insert <= noSample) {
				for (int k = noSample; k > insert; --k) {
					mins[k] = mins[k - 1];
					mins2[k] = mins2[k - 1];
					result[k][0] = result[k-1][0];
					result[k][1] = result[k-1][1];
				}

				mins[insert] = distance;
				mins2[insert] = distance2;
				result[insert][0] = j;
				result[insert][1] = j + interval - 1;
			}
		}
		
		result[noSample + 1] = mins2;
		result[noSample + 2] = mins;
		return result;
	}

	public static double distance(ArrayList<Double> x1, ArrayList<Double> x2) {
		if (x1.size() != x2.size()) {
			return 0;
		}
		
		double result = 10e-5;
		
		for (int i = 0; i < x1.size(); ++i) {
			result += Math.pow(x1.get(i) - x2.get(i),2);
		}
		
		return Math.sqrt(result);
	}
	
	public static double mean (ArrayList<Double> x) {
		double mean = 0;
		for (Double e : x) {
			mean += e;
		}
		mean /= x.size();
		return mean;
	}
	
	public static double std (ArrayList<Double> x) {
		double variance = 10e-5;
		double mean = mean(x);
		for (Double e : x) {
			variance += (e - mean)*(e - mean);
		}
		variance /= x.size();
		return Math.sqrt(variance);
	}
	
	public static ArrayList<Double> normalize(ArrayList<Double> x) {
//		double mean = mean(x);
//		double std = std(x);
//		
//		ArrayList<Double> result = new ArrayList<Double>();
//		
//		for (int i = 0; i < x.size(); ++i) {
//			result.add((x.get(i) - mean)/std);
//		}		
	
		ArrayList<Double> result = new ArrayList<Double>();
		
		for (int i = 0; i < x.size()-1; ++i) {
			result.add((x.get(i+1) - x.get(i))/x.get(i));
		}		

		
		return result;
	}

	public static ArrayList<Double> normalize2(ArrayList<Double> x) {
		double mean = mean(x);
		double std = std(x);
		
		ArrayList<Double> result = new ArrayList<Double>();
		
		for (int i = 0; i < x.size(); ++i) {
			result.add((x.get(i) - mean)/std);
		}		
	
		return result;
	}
	
	
	public double getInterval() {
		return interval;
	}
	public void setInterval(int interval) {
		this.interval = interval;
	}
	public HashMap<AssetEntity, ArrayList<PriceEntity>> getReferences() {
		return references;
	}
	public void setReferences(HashMap<AssetEntity, ArrayList<PriceEntity>> references) {
		this.references = references;
	}
	public ArrayList<PriceEntity> getPriceEntities() {
		return priceEntities;
	}
	public void setPriceEntities(ArrayList<PriceEntity> priceEntities) {
		this.priceEntities = priceEntities;
	}
	public int getNoSample() {
		return noSample;
	}
	public void setNoSample(int noSample) {
		this.noSample = noSample;
	}

}
